CompilerOld.php

<?php

namespace Phad;

/**
 * This thing is ... kinda trash ... but i just wanna keep it around for historical purposes ... i don't think it will ever be useful again ... but it will be cool to look at a few years from now & see how bad my code was lol
 * - Reed (feb 4, 2022)
 *
 */
class Compiler {

    protected string $source;
    protected array $output = [
        'view'=>null,
        'routes'=>null,
        // 'idk what else'=>null,
    ];
    protected array $routes = [
    
    ];

    public function __construct(string $source){
        $this->source = $source;
        $this->compile();
    }

    protected function getPropNodes($item){
        if ($item->tagName=='form')return $item->ownerDocument->xpath('descendant::*[@name]',$item);
        else return $item->ownerDocument->xpath('descendant::*[@prop]', $item);
    }

    protected function getAccessNodes($item){
        return $item->ownerDocument->xpath('descendant::access',$item);
    }

    protected function getStatusNodes($item){
        return $item->ownerDocument->xpath('descendant::on', $item);
    }
    protected function getErrorNodes($item){
        return $item->ownerDocument->xpath('descendant::error', $item);
    }

    protected function indent(string $str, int $numSpaces, $padFirstLine = true){
        $str = explode("\n", $str);
        $pad = str_pad('',$numSpaces);
        $str = implode("\n$pad",$str);
        if ($padFirstLine)$str = $pad.$str;
        return $str;
    }

    public function routes(){
        return $this->routes;
    }

    public function compile(){
        $source = $this->source;
        $phtml = new \Taeluf\PHTML($source);


        //build sitemap array from sitemap nodes
        $sitemapNodeList = $phtml->xpath('//sitemap');
        $sitemaps = [];
        foreach ($sitemapNodeList as $sn){
            $pattern = $sn->parentNode->pattern;
            // $sitemap = ['pattern'=>$pattern];
            $sitemal = [];
            foreach ($sn->attributes() as $attr){
                $sitemap[$attr->name] = $attr->value;
            }
            $sitemap['pattern']=$pattern;
            $sitemaps[$pattern] = $sitemap;
            $sn->parentNode->removeChild($sn);
        }
        // var_export sitemaps array into the compiled output & conditionally return the sitemap data

        $code = "<?php if ((\$phad_mode??null)==='get_sitemap_data'):";
        $code .= "\n    return ".var_export($sitemaps,true).';';
        $code .= "\nendif; // close get_sitemap_data section ?>";


        // $code = '<?php $this->sitemap = '.var_export($sitemaps,true).'; ';
        // $code .= "\nif ((\$phad_mode??null)==='get_sitemap_data')return \$this->routes;";
        $phtml->insertCodeBefore($phtml->childNodes[0]->childNodes[0]??null, $code);

        

        // build routes array from route nodes
        $routeNodeList = $phtml->xpath('//route');
        $routes = [];
        foreach ($routeNodeList as $rn){
            $route = [];
            foreach ($rn->attributes() as $attr){
                $route[$attr->name] = $attr->value;
            }
            $routes[] = $route;
            $rn->parentNode->removeChild($rn);
        }
        $this->routes = $routes;
        // var_export routes array into the compiled output & conditionally return the routes
        $code = '<?php $this->routes = '.var_export($routes,true).'; ';
        $code .= "\nif ((\$phad_mode??null)==='get_routes')return \$this->routes;";
        $code .= "\n?>";
        $phtml->insertCodeBefore($phtml->childNodes[0]->childNodes[0]??null, $code);


        
        //compile item nodes
        $itemNodeList = $phtml->xpath('//*[@item]');
        $limit = 50;
        $iter = 0;
        while ($iter++<$limit&&count($itemNodeList)>0){
            foreach ($itemNodeList as $index=>$itemNode){
                $children = $phtml->xpath('descendant::*[@item]', $itemNode);
                //@bugfix(jan 18, 2022) form's with nested items were NOT getting their <onsubmit> included when using this strategy for handling child items, so I added the ->tagName!=form check. This means forms' nested items are handled differently & this MAY be problematic.
                if (count($children) > 0
                    && $itemNode->tagName!='form'
                )continue;
                unset($itemNodeList[$index]);
                $this->compileItemNode($itemNode);
            }
        }

        // compile individual nodes with access like `<a href="/whatever/" access="role:admin">...</a>`
        $accessNodeList = $phtml->xpath('//*[@access]');
        foreach ($accessNodeList as $accessNode){
            $this->compileNodeWithAccess($accessNode);
        }

        $this->output['view'] = $phtml.'';
    }

    public function output(){
        return $this->output;
    }

    protected function compileNodeWithAccess($node){

        $node_info = $node->attributesAsArray();
        $node_info['tagName'] = $node->tagName;
        $node_info = var_export($node_info,true);
        $node->doc->insertCodeBefore($node, "<?php if (\$phad->can_read_node($node_info)): ?>\n");
        $node->doc->insertCodeAfter($node, "\n<?php endif; ?>");
        unset($node->access);
    }

    protected function compileItemNode($itemNode){
        // convenience vars
        $phtml = $itemNode->ownerDocument;
        $phpClose = '?>'; // stops my editor from being upset
        $phpOpen = '<?php'; //just mildly convenient
        $itemName = $itemNode->item;
        $itemListVar = "\$${itemName}List";
        $itemVar = "\$${itemName}";
        $itemDataVar = "\$${itemName}Item";
        $accessVar = "\$${itemName}Access";
        $accessListVar = "\$${itemName}AccessList";
        $code = [];

        // special nodes
        $propNodes = $this->getPropNodes($itemNode);
        $accessNodes = $this->getAccessNodes($itemNode);
        $statusNodes = $this->getStatusNodes($itemNode);
        $errorNodes = $this->getErrorNodes($itemNode);

        foreach ($errorNodes as $en){
            $errorNodeCode = "<?=is_array(\$phad->failed_submit_columns) ? \n"
                   ."    \$phad->validationErrorMessage(\$phad->failed_submit_columns) : ''?>";
            $en->doc->insertCodeBefore($en, $errorNodeCode);
        }

        // itemdata code
        $code[] = $this->getItemDataCode($itemDataVar, $itemName, $itemNode, $accessNodes);
        $isForm = $itemNode->is('form');

        // if ($isForm){
        // }

        // props code
        $itemDataProperties = [];
        $form_has_file_input = false;
        foreach ($propNodes as $pn){
            if ($isForm){
                if ($pn->tagName=='input'&&$pn->type=='file'){
                    $form_has_file_input = true;
                }
                $propsData = $pn->attributesAsArray();
                unset($propsData['prop']);
                unset($propsData['name']);
                $propsData['tagName'] = $pn->tagName;
                if ($pn->is('select')){
                    foreach($pn->xpath('option') as $optNode){
                        if (!$optNode->hasAttribute('value'))continue;
                        $propsData['options'][] = $optNode->value;
                    }
                }

                $itemDataProperties[$pn->prop ?? $pn->name] = $propsData;
            }
            $this->compilePropNode($itemNode, $pn);
        }

        if ($isForm){
            $itemNode->action = $itemNode->action ?? '';
            $itemNode->method = $itemNode->method ?? 'POST';
            if ($form_has_file_input){
                $itemNode->enctype = "multipart/form-data";
            }

            if (!isset($itemDataProperties['id'])){
                $itemDataProperties['id'] = ['tagName'=>'input', 'type'=>'hidden'];
            }
            $itemDataPropertiesExported = var_export($itemDataProperties, true);
            $code[] = $this->indent("${itemDataVar}->properties = $itemDataPropertiesExported;",4);
        }
        $code[] = <<<PHP
            if (${itemDataVar}->phad_mode == 'get_item_data'){
                return ${itemDataVar};
            }
            \$phad->resolveAccess($itemDataVar); 
        PHP;

        if ($isForm&&isset($itemNode->deleteable)){
            $code[] = <<<PHP
                if (${itemDataVar}->phad_mode == 'delete'){
                    \$phad->delete(${itemDataVar});
                    return;
                }
            PHP;
        }

        $code[] = 
        "    \$phad->itemListStarted($itemDataVar);"
            ;

        $onSubmitForm = '';
        if ($isForm){
            $onSubmitForm = '//<onsubmit> code goes here';
            $onSubmitNode = $itemNode->xpath('onsubmit')[0] ?? null;
            if ($onSubmitNode!==null){
                $onSubmitCode = $onSubmitNode->innerHTML;
                $placeholder = $onSubmitNode->innerHTML;
                $placeheldCode= $onSubmitNode->doc->codeFromPlaceholder($placeholder);
                if ($placeheldCode!==null){
                    $onSubmitCode = $placeheldCode;
                }
                $onSubmitForm = $phpClose.trim($onSubmitCode).$phpOpen;
                $onSubmitForm = trim($this->indent($onSubmitForm, 8));
            }
        }
        // display code (dealing with access index & status)
        $submit_code = <<<PHP

                    if ({$itemDataVar}->phad_mode == 'submit'){
                        $onSubmitForm
                        if ({$itemDataVar}->phad_mode=='submit'
                            && \$phad->submit($itemDataVar, ${itemVar}Row) )return;
                    }
        PHP;
        if ($itemNode->tagName!='form')$submit_code = '';
        $foreachDisplayCode = <<<PHP

                foreach(${itemDataVar}->list as ${itemVar}Row_Index=>${itemVar}Row): 
                    $itemVar = \$phad->objectFromRow($itemDataVar, ${itemVar}Row);
                    if (!\$phad->hasRowAccess($itemDataVar, $itemVar))continue;
                    {$submit_code}
                    \$phad->rowStarted($itemDataVar, $itemVar);
                    $phpClose

        PHP;
        $statusNodeData = $this->getStatusNodeData($statusNodes, $foreachDisplayCode, $phtml);

        $this->cleanupNodes($itemNode, $propNodes, $accessNodes, $statusNodes);

        $this->getDisplayLogicCode($beforeCode, $afterCode, $statusNodeData, $itemName, $phtml);
        $code[] = $beforeCode;
        $code = implode("\n", $code);
        $phtml->insertCodeBefore($itemNode, $code);
        $phtml->insertCodeAfter($itemNode, $afterCode);

    }

    protected function compilePropNode($item, $prop){
        $phtml = $item->ownerDocument;
        $itemName = $item->item;
        $p = $prop;
        $propName = $p->hasAttribute('prop') ? $p->prop : $p->name;
        $propCode = '$'.$itemName.'->'.$propName;

        $phpCode = null;
        if ($p->hasAttribute('filter')){
            $filterCode = var_export($p->filter,true);
            $phpCode = '<?=$phad->filter('.$filterCode.','.$propCode.')?>';
        } else {
            $phpCode = '<?='.$propCode.'?>';
        }

        if ($p->tagName == 'input' && $p->type == 'backend'){

        } elseif ($p->tagName=='input'){
            $p->value = $phtml->phpPlaceholder($phpCode);
        } else if ($p->tagName=='select'){
            $options = $phtml->xpath('descendant::option', $p);
            foreach ($options as $opt){
                $optVal = var_export($opt->value,true);
                $code = "<?=($optVal==$propCode)? ' selected=\"\" ' : ' '?>";
                $phtml->addPhpToTag($opt, $code);
            }
        } 
        else {
            $p->innerHTML=$phtml->phpPlaceholder($phpCode);
        }

        unset($p->filter);
        unset($p->prop);
    }

    protected function cleanupNodes($itemNode, $propNodes, $accessNodes, $statusNodes){
        unset($itemNode->item);
        if ($itemNode->tagName=='x-item'){
            $itemNode->hideOwnTag = true;
        } else if ($itemNode->is('form')){
            unset($itemNode->target);
            unset($itemNode->deleteable);
        }

        // prop nodes are cleaned up earlier on... when prop nodes are compiled
        // foreach ($propNodes as $p){
            // unset($p->prop);
        // }
        foreach ($accessNodes as $a){
            $a->parentNode->removeChild($a);
        } 
        foreach ($statusNodes as $s){
            $s->parentNode->removeChild($s);
        }
        foreach ($propNodes as $p){
            if ($p->tagName=='input' && $p->type == 'backend'){
                $p->parentNode->removeChild($p);
            }
        }

        foreach ($itemNode->xpath('//onsubmit') as $node){
            $node->parentNode->removeChild($node);
        }

        foreach ($itemNode->xpath('//error') as $errorNode){
            $errorNode->parentNode->removeChild($errorNode);
        }


        foreach ($itemNode->xpath('//x-prop') as $xPropNode){
            $xPropNode->hideOwnTag = true;
        }
    }

    protected function getItemDataCode($itemDataVar, $itemName, $itemNode, $accessNodes){
        $code = ['<?php '];
        /** init the the item data var */

        $formTarget = (function() use ($itemNode){
            if (!$itemNode->is('form')){ //|| $itemNode->target==null){
                // return ', "uhoh"=>false';
                return ", 'item_type'=>'view'";
                // return '';
            }
            $export = var_export($itemNode->target, true);
            $targetCode = ", 'target'=> $export, 'item_type'=>'form'";
            return $targetCode;
        })();

        $deleteable = '';
        if (isset($itemNode->deleteable)&&$itemNode->is('form')){
            $deleteable = 
                ", 'deleteable'=> "
                    . var_export($itemNode->deleteable, true); 
        }

        $code[] = $this->indent("$itemDataVar = (object)['accessList'=>[], 'args'=>\$args, 'list'=>[], 'name'=>'$itemName', 'accessStatus'=>false, 'phad_mode'=>\$phad_mode??'display'${formTarget}${deleteable}];",4);


        /** [200 => [accessIndex => $codeToExecute]] */
        $statusCode = [];
        $accessIndex = -1;
        foreach ($accessNodes as $a){
            $accessIndex++;
            $statusNodes = $a->ownerDocument->xpath('descendant::on',$a);
            $accessArray = [];
            foreach ($a->attributes() as $index=>$attr){
                $accessArray[$attr->name] = $attr->value;
            }
            $accessExported = var_export($accessArray,true);
            $accessExported = $this->indent($accessExported, 4, false);
            $code[] = <<<PHP
                ${itemDataVar}->accessList[] = $accessExported;
            PHP;
            // the status node needs this, later
            $a->setAttribute('accessIndex', $accessIndex);
        }
        
        return implode("\n", $code);
    }


    protected function getStatusNodeData($statusNodes, $displayCode, $phtml){
        // The target format:
        // [
            // 200 => [
                // [2]=>$theActualCode, // 2 is the accessIndex
                // ['else']=>$actualCode,
                // ['then']=>$theForeachCode,
            // ],
//
        // ];
        
        $statusNodeData = [];
        foreach ($statusNodes as $sn){
            if (strtolower($sn->parentNode->tagName)=='access'){
                $statusNodeData[$sn->getAttribute('s')][$sn->parentNode->accessIndex] = $phtml->placeholder($sn->innerHTML);
            } else {
                $statusNodeData[$sn->getAttribute('s')]['else'] = $phtml->placeholder($sn->innerHTML);
            }
        }
        $statusNodeData[200]['then'] = $displayCode;

        $snd = $statusNodeData;
        foreach($snd as $status=>$list){
            uasort($list, function($a, $b){
                if (is_string($a))return -1;
                else if (is_string($b))return 1;
            });
            $statusNodeData[$status] = $list;
        }

        return $statusNodeData;
    }

    public function getDisplayLogicCode(&$beforeCode, &$afterCode, $statusNodeData, $itemName, $phtml){

        $itemDataVar = "\$${itemName}Item";
        $itemVar = "\$${itemName}";

        $closeTag = '?>';

        $if = 'if';
        $beforeCode = [];
        $beforeCode[] = <<<PHP
            if (${itemDataVar}->accessStatus == 200):
        PHP;
        $hasIf = false;
        foreach ($statusNodeData[200] as $accessIndex=>$actualCode){
            if (is_numeric($accessIndex)){
                $hasIf = true;
                $beforeCode[] = <<<PHP
                        ${if} (${itemDataVar}->accessIndex == $accessIndex): $closeTag
                            $actualCode
                        <?php
                PHP;
                $if = 'elseif';
            } else if ($accessIndex=='else'){
                if ($hasIf){
                    $beforeCode[] = <<<PHP
                            else: $closeTag
                                $actualCode
                            <?php
                    PHP;
                } else {
                    $beforeCode[] = <<<PHP
                            if (true): $closeTag
                            $actualCode
                            <?php
                    PHP;
                }

                $hasIf = true;
            } else if ($accessIndex=='then'){
                if ($hasIf){
                    $beforeCode[] = $this->indent('endif; //close access status = 200 & access index = 0', 8);
                }
                $beforeCode[] = $actualCode;
                continue;
            } 
        }

        $beforeCode = implode("\n", $beforeCode);

        $afterCode = [];
        $afterCode[] = <<<PHP

                    <?php 
                    \$phad->rowFinished($itemDataVar, $itemVar);
                endforeach;
        PHP;

        $hasAccessIf = false;
        foreach ($statusNodeData as $accessStatus=>$accessIndexList){
            if ($accessStatus==200)continue;
            $afterCode[] = <<<PHP
                elseif (${itemDataVar}->accessStatus == $accessStatus):
            PHP;
            $if = 'if';
            // $count = count($accessIndexList);
            // $iters = 0;
            $if_iters = 0;
            foreach ($accessIndexList as $accessIndex=>$actualCode){
                // $iters++;
                if (is_numeric($accessIndex)){
                    $if_iters++;
                    // $endif = $iters < $count
                        // ? 'endif; //close if (\$NamedItem->accessIndex == ###)'
                        // : '// no endif, expecting else';
                    $afterCode[] = <<<PHP
                            ${if} (${itemDataVar}->accessIndex == $accessIndex): $closeTag
                                $actualCode
                            <?php
                    PHP;
                    $afterCode[] = '        endif; // added to close accessIndex if()';
                    $if = 'elseif';
                    $hasAccessIf = true;
                } else if ($accessIndex=='else'){
                    
                    if ($if_iters>0)array_pop($afterCode);
                    $else = $hasAccessIf ? 'else: ' : '';
                    $afterCode[] = <<<PHP
                            $else $closeTag
                                $actualCode
                            <?php
                            endif; // close else: within access branches
                    PHP;
                    // $hasAccessIf = true;
                } 
            }

        }
        $accessIf = $hasAccessIf ? 'endif; //idk' : '';
        $accessIf = '//endif; removed';
        $afterCode[] = 
            <<<PHP
                    $accessIf
                endif; // close sequence of elseif (\$NamedItem->accessStatus == ###):
        
                \$phad->itemListFinished($itemDataVar);
            PHP
        ;
        $afterCode[] = '?>';
        $afterCode = implode("\n", $afterCode);

    }

}